iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用系列 第 3

命令進入 CLI 程式後:Command、Arg 與 Flag 的拆解與設計

  • 分享至 

  • xImage
  •  

上一篇我們追蹤了一行命令在進到程式之前發生的事:終端機接收鍵盤輸入、Shell 解析文字並尋找執行檔、作業系統建立 Process 並將參數陣列傳給程式。當控制權正式移交給我們的 CLI 程式後,程式收到的只是一串原始字串陣列(argv)。Shell 並不知道各個字串的業務語意,程式的第一步就是將這串無標籤的文字拆解為有意義的介面元素。

站在 CLI 開發者的視角,一行命令主要由 Command(動作)、Arg(操作對象)與 Flag(選項)三個核心組成分工而成。

以下以常見的 git 命令為例,展示兩組典型命令在解析後的結構對照:在 git commit -m "fix bug" 中,-m"fix bug" 屬於修飾執行行為的 Flag 與其數值;而在 git checkout main 中,main 則是直接指定操作目標的 Arg。兩組命令對照呈現了 CLI 解析器如何將傳入的參數,區分為調整行為的選項與指定目標的對象:

https://ithelp.ithome.com.tw/upload/images/20260914/20111896iDJxZNqtvS.png

當控制權移交給程式後,解析器會依據語意將傳入的 argv 拆解為三種不同職責的介面元素:

Command(命令)

Command 是整個 CLI 的「動詞」,代表使用者想要執行的核心功能,決定程式該走入哪一條執行路徑。

在 CLI 設計中,Command 依複雜度有兩種主要型態:

  • 單一命令(Single-command):例如 curlcatjq。這類工具功能專一,執行檔檔名本身(Root Command)就是唯一的動作,直接接收 Flag 與 Arg,不需要額外拆分子命令。
  • 多命令架構(Multi-command):例如 gitdockerkubectl。當工具涵蓋的功能眾多時,執行檔本身只作為總入口,底下再延伸出多個特定功能的子命令(Subcommand),例如 git commit 只專注於建立提交,git push 專注於推送。

無論是頂層命令還是子命令,Command 都具備以下核心特性:

  • 獨立的領域邊界:每個 Command 本質上都是一個獨立的子程式邏輯,擁有專屬的業務處理、參數驗證規則與 --help 說明文件。
  • 層級化管理:當功能進一步擴充時,Subcommand 可以組合成階層結構,例如 git remote adddocker compose up,將相同領域資源的操作歸類在同一個父命令下。

Arg(位置參數)

Arg 是直接跟在 Command 後面的值,用來指定這次動作要作用的具體目標或資源:

  • 直接指向目標:例如 git checkout main 中的 main 是要切換的分支,或是 rm file.txt 中的 file.txt 是要刪除的檔案。
  • 嚴格的位置順序性:Arg 是沒有名稱前綴的位置參數,程式靠出現在 argv 陣列中的位置來判讀意義。因此 Arg 的順序不能隨意調換,例如 cp source.txt dest.txt 若顛倒為 cp dest.txt source.txt,操作的方向與結果就會完全相反。

Flag:調整動作的「執行方式」

Flag 是對 Command 動作的修飾,用來調整程式執行的選項與行為細節:

  • 長 Flag 與短 Flag:Flag 包含長格式(以 -- 開頭的全稱單字,如 --message)與短格式(以 - 開頭的單字母縮寫,如 -m)。長 Flag 可讀性高、適合寫入腳本或自動化流程;短 Flag 則方便使用者在終端機中快速輸入。
  • 短 Flag 的組合語法(Clustering):當多個短 Flag 都是不需要帶值的布林選項時,POSIX 標準習慣允許將它們組合在同一個 - 後面。例如 ls -l -a -h 可以簡寫為 ls -lahtar -x -v -f 可以簡寫為 tar -xvf。若組合中包含需要接值的短 Flag(如 -m "message"),該 Flag 必須放在組合的最後一位(例如 -am "message")。
  • 布林 Flag 與反向開關:布林型態的 Flag 不需要指定值,只要出現在命令中即代表開啟(true),例如 git commit --amend。當某個選項的預設值本身就是 true 時,CLI 的設計慣例是提供帶有 --no- 前綴的反向開關(如 git commit --no-verify 用來關閉 Hook 檢查),或是支援 --verify=false 顯式關閉。

三者的核心特性對照如下:

  • Command:代表做什麼的動作。在命令中遵循固定的階層結構,沒有前綴標籤,通常為必填。
  • Arg:代表對誰做的操作對象。程式嚴格依位置順序解析,沒有前綴標籤,通常為必填。
  • Flag:代表怎麼做的執行選項。在命令中沒有順序限制,帶有 --- 前綴標籤,通常為選填且具備預設值。

依操作性質選擇 Arg、Flag 與 Subcommand

在設計 CLI 介面時,最常面臨的兩個架構抉擇是「這該做成 Arg 還是 Flag?」以及「這該做成 Subcommand 還是 Flag?」。

抉擇一:Arg 還是 Flag?

關鍵在於資料是否具備「單一明確的目標性質」,還是「多個無順序的屬性設定」。

  • 使用 Arg 的情境
    • 操作目標單一且明確:例如 git checkout main 中的分支名稱。這是因為動作與目標已經構成直覺的「動詞 + 受詞」關係,語意非常清晰。
    • 同性質資源的批次處理:例如 rm a.txt b.txt。這是因為每個傳入項目的性質與角色完全相同,彼此沒有順序歧義。Arg 能自然接收不定長度的參數列表;若改用 Flag 重複指定,例如 rm --file a.txt --file b.txt,會讓命令變得極度冗長且反直覺。
  • 使用 Flag 的情境
    • 輸入資料包含多種不同性質的屬性,且沒有自然的先後順序。
    • 若強行使用 Arg,會逼使用者死記參數位置,例如 mytool Alice 42 admin。這種情況應改用具名 Flag 明確標註語意:
      mytool --name Alice --age 42 --role admin
      

抉擇二:Subcommand 還是 Flag?

關鍵在於新功能是「一個全新的動作」,還是「既有動作的細節變化」。

  • 使用 Subcommand 的情境
    • 行為本質根本不同,且擁有各自專屬的參數邏輯。例如 git commitgit push 職責完全不同,必須劃分為不同命令。
    • 常見反模式:避免用 Flag 切換核心操作模式。例如 mytool --mode=create 應重構為語意更清晰的子命令 mytool createmytool list
    • 當同一組資源包含多種操作時,可以使用巢狀子命令來組織命名空間,例如 git remote add
  • 使用 Flag 的情境
    • 行為本質相同,只是調整執行的細節或微調行為。例如以下兩者本質都是「提交」,因此透過 Flag 調整細節即可:
      git commit --message "fix bug"
      git commit --amend
      

回應結果與交還控制權(stdout、stderr 與 Exit Code)

當 CLI 程式解析完 Command、Arg 與 Flag 並執行完業務邏輯後,最後一步是「將結果回傳給使用者與作業系統」,完成整個執行生命週期。

CLI 的輸出並非單純「將字串印在螢幕上」,而是同時面向使用者與自動化系統,包含兩個截然不同的回饋管道:

  1. 資料流(stdout / stderr):內容印在哪個頻道,決定了下游程式能不能正確解析。
  2. 狀態碼(Exit Code):結束時留給系統的數字,決定了自動化腳本能不能判斷成功或失敗。

輸出頻道分流:stdout vs stderr

作業系統在啟動每個程式時,預設會提供兩個獨立的輸出管道(在 Unix 底層稱為檔案描述符 FD 1 與 FD 2):

  • stdout(標準輸出,FD 1):輸出「命令產出的結果資料」。也就是呼叫這支程式真正想要拿到的內容,例如查詢出的清單、計算出的數值、或是產出的 JSON。
  • stderr(標準錯誤,FD 2):輸出「過程中的狀態與錯誤提示」。例如連線提示、下載進度條、警告,以及程式出錯時的報錯原因。

為什麼要分開?

兩者的關鍵差別在於:「這是最終產出的資料,還是執行過程的訊息」

如果沒有分開,執行過程中的提示文字就會直接污染資料流。只有將兩者切開,呼叫端(使用者、Shell 腳本或其他工具)才能依需求自由控制輸出:

  • 重導向至檔案(>mytool export > data.json
    只有 stdout 的結果資料會被寫入檔案;而 stderr 上的連線或進度提示依然會即時顯示在螢幕上,不會污染檔案內容。
  • 管線串接(|mytool export | jq .
    管線預設只會擷取 stdout 傳給下一個工具。如果把「連線中...」也混進 stdout,下游的 jq 就會因讀到非 JSON 格式的文字而解析失敗。
  • 獨立過濾日誌(2>mytool export 2> /dev/null | jq .
    若不需要過程訊息,呼叫端可以將 stderr 獨立重導向丟棄,只留下純淨的資料流繼續往下串接。

因此,CLI 開發的核心原則很明確:命令產出的結果資料走 stdout,其餘任何過程提示、除錯日誌與錯誤訊息通通走 stderr

Exit Code:機器之間的通用溝通暗號

程式結束退出時,必須向作業系統交還一個整數數值(範圍通常為 0 ~ 255),稱為 Exit Code(退出狀態碼):

  • 0 代表成功(Success):一切順利完成。
  • 0(1 ~ 255)代表失敗(Failure):常見如 1 為通用錯誤、2 為參數語法錯誤(例如少傳必填 Flag)。

人類判斷指令是否成功,是靠眼睛閱讀終端機印出的英文字句(如 Success!Error: not found);但 Shell 腳本、CI/CD 流程與 AI Agent 無法也不該依賴字串比對,它們完全仰賴 Exit Code 來決定後續流程

1. 條件連鎖與自動化判斷

在 Shell 腳本中,常見的連鎖運算符會根據前一個指令的 Exit Code 決定下一步:

# 只有當 build 成功回傳 0 時,後續的 deploy 才會被觸發
mytool build && mytool deploy

# 若備份失敗(回傳非 0),則觸發報警
mytool backup || send-alert "Backup failed!"

在 CI/CD(如 GitHub Actions)中,流水線之所以知道某個測試步驟失敗並亮紅燈,也完全是因為該指令結束時的 Exit Code 不是 0。

2. CLI 開發者最常見的反模式:吃掉錯誤

在撰寫 CLI 程式時,最嚴重的 Bug 之一就是「捕捉了錯誤、印出紅字,卻以 Exit Code 0 正常結束」:

  • 終端機畫面上雖然印著醒目的 Error: failed to connect
  • 但因為 Exit Code 依然是 0,外層的 CI/CD 判定該步驟為「成功綠燈」,導致整個部署流程帶著錯誤繼續上線。
  • 只要命令未達預期成果,除了在 stderr 印出錯誤原因外,務必明確以非 0 狀態碼退出(例如 Go 的 os.Exit(1))。

一行 CLI 命令在進入程式後的運作與設計至此已完整閉環:從接收原始 argv 拆解語意,到執行完畢將結果透過 stdout/stderr 與 Exit Code 回應給系統。下一篇我們將設定 Go 開發環境,並用 Cobra 把這套 Command、Arg 與 Flag 寫成第一個可執行的 CLI 專案。


上一篇
命令進入 CLI 程式前:終端機、Shell 與 OS 的運作原理
下一篇
建立第一個 CLI 專案
系列文
30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言